/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.portlets.portletadmin.xmlsupport;

import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.portal.portlet.om.IPortletType;
import org.apereo.portal.portlet.registry.IPortletTypeRegistry;
import org.apereo.portal.portletpublishing.xml.PortletPublishingDefinition;
import org.apereo.portal.portletpublishing.xml.Step;
import org.apereo.portal.utils.threading.MapCachingDoubleCheckedCreator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

@Service("channelPublishingDefinitionDao")
public class XmlChannelPublishingDefinitionDao
        implements IChannelPublishingDefinitionDao, ResourceLoaderAware, InitializingBean {

    private static final String SHARED_PARAMETERS_PATH =
            "/org/apereo/portal/portlets/SharedParameters.cpd.xml";

    protected final Log logger = LogFactory.getLog(this.getClass());

    private Unmarshaller unmarshaller;

    private MapCachingDoubleCheckedCreator<Integer, PortletPublishingDefinition> cpdCreator;
    private List<Step> sharedParameters;

    private Map<Integer, PortletPublishingDefinition> cpdCache;
    private IPortletTypeRegistry portletTypeRegistry;
    private ResourceLoader resourceLoader;

    public XmlChannelPublishingDefinitionDao() throws JAXBException {
        JAXBContext context = JAXBContext.newInstance(PortletPublishingDefinition.class);
        unmarshaller = context.createUnmarshaller();
    }

    @javax.annotation.Resource(name = "cpdCache")
    public void setCpdCache(Map<Integer, PortletPublishingDefinition> cpdCache) {
        this.cpdCache = cpdCache;
    }

    @Autowired(required = true)
    public void setPortletTypeRegistry(IPortletTypeRegistry portletTypeRegistry) {
        this.portletTypeRegistry = portletTypeRegistry;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.cpdCreator = new CpdCreator(this.cpdCache);
    }

    /* (non-Javadoc)
     * @see org.apereo.portal.portlets.portletadmin.xmlsupport.IChannelPublishingDefinitionDao#getChannelPublishingDefinition(int)
     */
    @Override
    public PortletPublishingDefinition getChannelPublishingDefinition(int channelTypeId) {
        return this.cpdCreator.get(channelTypeId);
    }

    /* (non-Javadoc)
     * @see org.apereo.portal.portlets.portletadmin.xmlsupport.IChannelPublishingDefinitionDao#getChannelPublishingDefinitions()
     */
    @Override
    public Map<IPortletType, PortletPublishingDefinition> getChannelPublishingDefinitions() {
        final List<IPortletType> channelTypes = this.portletTypeRegistry.getPortletTypes();

        final Map<IPortletType, PortletPublishingDefinition> cpds =
                new LinkedHashMap<IPortletType, PortletPublishingDefinition>(channelTypes.size());

        for (final IPortletType channelType : channelTypes) {
            final PortletPublishingDefinition cpd =
                    this.getChannelPublishingDefinition(channelType.getId());
            cpds.put(channelType, cpd);
        }

        return cpds;
    }

    private PortletPublishingDefinition loadChannelPublishingDefinition(int channelTypeId) {
        // if the CPD is not already in the cache, determine the CPD URI
        final String cpdUri;
        if (channelTypeId >= 0) {
            final IPortletType type = this.portletTypeRegistry.getPortletType(channelTypeId);
            if (type == null) {
                throw new IllegalArgumentException(
                        "No ChannelType registered with id: " + channelTypeId);
            }
            cpdUri = type.getCpdUri();
        } else {
            throw new IllegalArgumentException(
                    "No ChannelType registered with id: " + channelTypeId);
        }

        // read and parse the CPD
        final PortletPublishingDefinition def;
        final Resource cpdResource = this.resourceLoader.getResource("classpath:" + cpdUri);
        if (!cpdResource.exists()) {
            throw new MissingResourceException(
                    "Failed to find CPD '" + cpdUri + "' for channel type " + channelTypeId,
                    this.getClass().getName(),
                    cpdUri);
        }

        final InputStream cpdStream;
        try {
            cpdStream = cpdResource.getInputStream();
        } catch (IOException e) {
            throw new MissingResourceException(
                    "Failed to load CPD '" + cpdUri + "' for channel type " + channelTypeId,
                    this.getClass().getName(),
                    cpdUri);
        }

        try {
            def = (PortletPublishingDefinition) this.unmarshaller.unmarshal(cpdStream);
            final List<Step> sharedParameters = this.getSharedParameters();
            def.getSteps().addAll(sharedParameters);
            // add the CPD to the cache and return it
            this.cpdCache.put(channelTypeId, def);

            return def;
        } catch (JAXBException e) {
        } finally {
            IOUtils.closeQuietly(cpdStream);
        }

        return null;
    }

    private List<Step> getSharedParameters() {
        if (this.sharedParameters != null) {
            return this.sharedParameters;
        }

        // read and parse the shared CPD
        final Resource paramResource =
                this.resourceLoader.getResource("classpath:" + SHARED_PARAMETERS_PATH);
        if (!paramResource.exists()) {
            throw new MissingResourceException(
                    "Failed to find shared parameters CPD '" + SHARED_PARAMETERS_PATH + "'",
                    this.getClass().getName(),
                    SHARED_PARAMETERS_PATH);
        }

        final InputStream paramStream;
        try {
            paramStream = paramResource.getInputStream();
        } catch (IOException e) {
            throw new MissingResourceException(
                    "Failed to load CPD '" + SHARED_PARAMETERS_PATH + "'",
                    this.getClass().getName(),
                    SHARED_PARAMETERS_PATH);
        }

        // parse the shared CPD and add its steps to the end of the type-specific
        try {
            PortletPublishingDefinition config =
                    (PortletPublishingDefinition) unmarshaller.unmarshal(paramStream);
            this.sharedParameters = config.getSteps();
        } catch (JAXBException e) {
            logger.warn("Failed to parse: " + paramResource, e);
        } finally {
            IOUtils.closeQuietly(paramStream);
        }
        return this.sharedParameters;
    }

    private class CpdCreator
            extends MapCachingDoubleCheckedCreator<Integer, PortletPublishingDefinition> {
        public CpdCreator(Map<Integer, PortletPublishingDefinition> cache) {
            super(cache);
        }

        @Override
        protected PortletPublishingDefinition createInternal(Integer key, Object... args) {
            return loadChannelPublishingDefinition(key);
        }

        @Override
        protected Integer getKey(Object... args) {
            return (Integer) args[0];
        }
    }
}
